#include "socket.hpp"

#include <cstring>
#include <cerrno>
#include <cstdio>
#include <cstdlib>

#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netdb.h>
#include <string>
#include <sstream>

using namespace std;

#define ERR(fun,code) ErrorMessage(__FILE__,__LINE__,__FUNCTION__,fun,code)

#define E(fun)  do {\
                        int e=errno;\
                        if(e)\
                            throw ERR(fun,e);\
                    }while(false)

string ErrorMessage(const char *file_name,size_t line,const char *parent_function,const char *function,int code); // 输出错误信息

socket_t::~socket_t()
{
    if(sock != -1) close(sock);
}

void init_addrinfo(addrinfo &hints, SOCK_TYPE type)
{
    switch(type)
    {
        case SOCK_TYPE::RAW:
            hints.ai_socktype = SOCK_RAW;
            hints.ai_protocol = IPPROTO_RAW;
            break;
        case SOCK_TYPE::TCP:
            hints.ai_socktype = SOCK_STREAM;
            hints.ai_protocol = IPPROTO_TCP;
            break;
        case SOCK_TYPE::UDP:
            hints.ai_socktype = SOCK_DGRAM;
            hints.ai_protocol = IPPROTO_UDP;
            break;
    }

}

void client_t::connect(const char *host, int port, SOCK_TYPE type)
{
    addrinfo hints = {0};
    hints.ai_family = AF_UNSPEC;
    init_addrinfo(hints, type);

    find_available_socket(host, port, hints);
}

void server_t::bind(const char *host, int port, SOCK_TYPE type)
{
    addrinfo hints = {0};
    hints.ai_family = AF_INET;
    hints.ai_flags = AI_PASSIVE;
    init_addrinfo(hints, type);

    find_available_socket(host, port, hints);
}

void server_t::listen(int backlog)
{
    ::listen(sock, backlog);
}

void server_t::listen()
{
    listen(SOMAXCONN);
}

void server_t::accept()
{
    int s = ::accept(sock, NULL, NULL);
    if(s == -1)
        E("accept");
    close(sock);
    sock = s;
}

void socket_t::shutdown(bool read, bool write)
{
    int how;
    if(read && write)
        how = SHUT_RDWR;
    else if(read)
        how = SHUT_RD;
    else if(write)
        how = SHUT_WR;
    else
        return;
    ::shutdown(sock, how);
}

int socket_t::send(const string &buf)
{
    int r = ::send(sock, buf.data(), buf.size(), 0);
    if(r == -1)
        E("send");
    return r;
}

string socket_t::recv()
{
    string s;
    int r;
    do {
        char buf[8];
        r = ::recv(sock, buf, sizeof buf, 0);
        if (r > 0)
            s.append(buf, buf + r);
        else if (r < 0)
            E("recv");
    } while(r > 0);
    return s;
}

void socket_t::find_available_socket(const char *host, int port, addrinfo &hints)
{
    class addrinfo_t
    {
        public:
            addrinfo_t(): d(nullptr){}
            ~addrinfo_t()
            {
                if(d)
                    freeaddrinfo(d);
            }
            addrinfo** operator&()
            {
                return &d;
            }
            operator addrinfo*()
            {
                return d;
            }
        private:
            addrinfo *d;
    } result;

    // Resolve the server address and port
    ostringstream oss;
    oss << port;
    int r = getaddrinfo(host, oss.str().c_str(), &hints, &result);
    if (r)
        throw ERR("getaddrinfo", r);

    // Attempt to connect to an address until one succeeds
    for(addrinfo *ptr = result; ptr; ptr = ptr->ai_next) {
        // Create a SOCKET for connecting to server
        sock = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
        if (sock == -1)
            E("socket");

        if(hints.ai_flags)
            r = ::bind(sock, ptr->ai_addr, ptr->ai_addrlen);
        else
            r = ::connect(sock, ptr->ai_addr, ptr->ai_addrlen);
        if (r != -1)
            return;
        sock = -1;
    }

    using namespace string_literals;

    if(sock == -1)
    {
        if(hints.ai_flags)
            throw "Unable to bind to port!"s;
        else
            throw "Unable to connect to server!"s;
    }
}

string ErrorMessage(const char *file_name,size_t line,const char *parent_function,const char *function,int code)
{
    ostringstream oss;
    oss << "Filename: " << file_name << endl
        << "Line: " << line << endl
        << "Parent function: " << parent_function << endl
        << "Function: " << function << endl
        << "Code: " << code;

    const char *s = strerror(code);

    if(s)
        oss << endl << "Description: " << s;

    return oss.str();
}